/**
 * @fileoverview Library for geographical areas indexation and accelerate search of a point in areas.
 * @version 1.0
 * @author philippe.debie@gmail.com
 * @license Copyright 2013 Philippe Debie
 * 
 * How to use :
 *  Init :
 *    {@code var a = new ClassSliceZone(<Areas geoJSON>, 10);} 10 is band width (km)
 *    {@code a.prepare_zone();}
 *
 *  Search :
 *    {@code a.search_pointinside({latitude: 51.5, longitude: 4.2});}
 *
 *  Reformated with http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
 */

/** 
 * @constructor ClassSliceZone
 *
 * @param {geojson}	geojson		A geojson structure of areas
 * @param {integer}	bw			Band Width
 * @return			None		None
 */
var ClassSliceZone = function (geojson, bw) {

	/** @private Bands width in kilometer default is 20km */
	if (typeof(bw) === 'undefined') this.bandWidth_ = 20; // this.bandWidth_ = bw || 20;
	else this.bandWidth_ = bw;
	
	/** @private Bands array of array */
	this.bands_ = [];
	
	/** @private Used to give coordinates to geolib functions */
	this.myarrayofobject_ = [];
	
	/** @private The minimum longitude point used to compute each centroid distance */
	this.medMinPoint_ = {};
	
	/** @public Use the property foundfeature to read information about the matching area */
	this.foundfeature = {};
	
	/** @public Contain min and max of longitude and latitude, and also median latitude  */
	this.MMCZ = {};
	this.gj = geojson;
	this.speedTest = {NbTestSuc: 0, NbTestFail: 0, Step: 1, _minLat: 0, _maxLat: 0, _minLng: 0, maxLng: 0};
	this.statMyBands = {};
}

/**
 * @public search_pointinside_gj Search a point in the given geoJSON structure
 * This search does NOT use the bands so it take a long time and can be used to compare time
 * @see search_pointinside() for the high speed bands search
 *
 * @param {object}			my_point	Coordinates like this {latitude: 45.5, longitude: 5.2}
 * @return {boolean}		Found is true
 * @return {object} 		Use the public property foundfeature to read information about the matching area
 */
ClassSliceZone.prototype.search_pointinside_gj = function(my_point) {
	this.foundfeature = {};
	for (var i = 0; i < this.gj.features.length; i++) {
		this.format_arraygeolib_(this.gj.features[i].geometry);
		if (geolib.isPointInside(my_point, this.myarrayofobject_)) {
			this.foundfeature = this.gj.features[i];
			return true;
		}		
	}
	return false;
}

/**
 * @private format_arraygeolib_ Format coordinates for geolib functions
 * Order in geojson is longitude and latitude. TAKE CARE of order
 * Only the first on multi-polygon is used. Should be better in next version.
 *
 * @param {geojson.features[].geometry}	mygeometry		Object type geojson.features[].geometry is an area
 * @return None							None
 */
ClassSliceZone.prototype.format_arraygeolib_ = function(mygeometry) {
	if (mygeometry.type === "MultiPolygon") {
		var myarrayofarray = mygeometry.coordinates[0][0];
	}
	else {
		var myarrayofarray = mygeometry.coordinates[0];
	}
	this.myarrayofobject_ = [];
	for (var j = 0; j < myarrayofarray.length; j++) {
		this.myarrayofobject_.push({latitude: myarrayofarray[j][1], longitude: myarrayofarray[j][0]});
		/** Order in geojson is longitude and latitude. TAKE CARE of order*/
	}
	return true;
}

/**
 * @private calc_centerzone_ Compute the centroid of each areas
 *
 * @param		None		None
 * @return		None		None
 */
ClassSliceZone.prototype.calc_centerzone_ = function() {
	for (var i = 0; i < this.gj.features.length; i++) {
		this.format_arraygeolib_(this.gj.features[i].geometry);		
		this.gj.features[i].properties.centerzone = geolib.getCenter(this.myarrayofobject_);
		// console.log(i + " ");		
	}	
}

/**
 * @private calc_minmax_ Compute the min max MMCZ structure
 *
 * @param		None		None
 * @return		None		None
 */
ClassSliceZone.prototype.calc_minmax_ = function() {
	var coords = [];
	for (var j = 0; j < this.gj.features.length; j++) {
		coords.push({latitude: this.gj.features[j].properties.centerzone.latitude, 
						longitude: this.gj.features[j].properties.centerzone.longitude});
	}
	this.MMCZ = geolib.getBounds(coords);
	this.MMCZ.medLat = (this.MMCZ.maxLat + this.MMCZ.minLat) / 2;
	this.medMinPoint_ = {latitude: this.MMCZ.medLat, longitude: this.MMCZ.minLng};
	// console.log("medMinPoint " +  this.medMinPoint_.latitude + " " + this.medMinPoint_.longitude);
	return true;
}


/**
 * @private calc_dist_ctzone_ Compute distance for each area centroid from min point
 * With distance store each area in her band
 * This function end bands preparation 
 * @param		None		None
 * @return		None		None
 */
ClassSliceZone.prototype.calc_dist_ctzone_ = function() {
	var point2_ = {};
	point2_.latitude = this.medMinPoint_.latitude;
	var MaxNumBand = 0;
	for (var i = 0; i < this.gj.features.length; i++) {
		// console.log(this.gj.features[i].properties.NAME); 
		point2_.longitude = this.gj.features[i].properties.centerzone.longitude;
		// console.log("Point 2 " +  point2_.latitude + " " + point2_.longitude);
		this.gj.features[i].properties.distCZ = geolib.getDistanceSimple(this.medMinPoint_, point2_, 10) / 1000;
		// console.log("DistCZ " + this.gj.features[i].properties.distCZ); 
		this.gj.features[i].properties.numBand = Math.floor(this.gj.features[i].properties.distCZ / this.bandWidth_);
		if (this.gj.features[i].properties.numBand > MaxNumBand) {
			MaxNumBand = this.gj.features[i].properties.numBand;
		}
		// console.log("NumBand " + this.gj.features[i].properties.numBand);
	}
	// console.log("MaxNumBand " + MaxNumBand);
	for (var i = 0; i < MaxNumBand+1; i++) {
		this.bands_[i] = [];
	}

	for (var i = 0; i < this.gj.features.length; i++) {
		this.bands_[this.gj.features[i].properties.numBand].push(this.gj.features[i]);
	}	
	
}

/**
 * @public prepare_zone prepare all bands needed to perform searchs
 ********************************************************************
 * Initialisation 
 *
 * @param {integer}	bw		You can redefine band width
 * @return			None	None
 *********************************************************************
 */
ClassSliceZone.prototype.prepare_zone = function(bw) {
	if (typeof(bw)!=='undefined') this.bandWidth_ = bw;
	console.log("Band width is ", this.bandWidth_);	
	this.bands_.length = 0;
	this.myarrayofobject_ = [];
	this.medMinPoint_ = {};
	
	this.MMCZ = {};	
	this.foundfeature = {};
	
	this.calc_centerzone_(); // Calculer le centre de chaque zone : CenterZone = (CZLat, CZLong)
	this.calc_minmax_(); // Trouver le min longitude des centres : MinCZLong
	// et Trouver le min max latitude des centres, trouver la latitude central des min max (latitude mediane : MedCZLat).
	this.calc_dist_ctzone_(); // calculer les distances entre le min des CZ et la longitude du centre
	return true;
}

/**
 * @private search_inBand_ Search in one given band
 *
 * @param				point		Search point coordinates 
 * @param {integer}		my_band		Band index 0, 1 or 2 or etc...
 * @return {boolean}	Band found is true
 */
ClassSliceZone.prototype.search_inBand_ = function(my_point, my_band) {
	// console.log("search_inBand_ " + my_band);
	if ((my_band >= 0) && (my_band < this.bands_.length)) {
		for (var i = 0; i < this.bands_[my_band].length; i++) {
			// console.log("Search Dep " + this.bands_[my_band][i].properties.NAME);
			this.format_arraygeolib_(this.bands_[my_band][i].geometry);
			if (geolib.isPointInside(my_point, this.myarrayofobject_)) {
				this.foundfeature = this.bands_[my_band][i];
				return true;
			}
		}
	}
	return false;
}

/**
 * @public search_pointinside The high speed band search point. This is the search function to use.
 *
 * @param		my_point	{latitude: 45.5, longitude: 5.2}
 * @return {boolean}		true if band found
 */
ClassSliceZone.prototype.search_pointinside = function(my_point) {
	// console.log("--- search_pointinside --- ");
	this.foundfeature = {};
	var point2_ = {};
	point2_.latitude = this.medMinPoint_.latitude;
	point2_.longitude = my_point.longitude;
	var dist = geolib.getDistanceSimple(this.medMinPoint_, point2_, 10) / 1000;
	var numBand = Math.floor(dist / this.bandWidth_);
	// console.log("numBand " + numBand);
	
	if (this.search_inBand_(my_point, numBand)) {
		return true;
	}
	var BottomBandReached = false;
	var TopBandReached = false;
	var Finish = false;
	var StepBand = 1;
	while (!Finish) {
		if (numBand + StepBand >= this.bands_.length) {
			if (!TopBandReached) {
				// console.log("TopBandReached numBand " + numBand + " StepBand " + StepBand + " length " + this.bands_.length);
			}
			TopBandReached = true;
		}
		else {
			if (this.search_inBand_(my_point, numBand + StepBand)) {			
				Finish = true;
				return true;
			}
		}
		
		if (numBand - StepBand < 0 ) {
			if (!BottomBandReached) {
				// console.log("BottomBandReached numBand " + numBand + " StepBand " + StepBand);
			}
			BottomBandReached = true;
		}
		else {
			if (this.search_inBand_(my_point, numBand - StepBand)) {				
				Finish = true;
				return true;
			}
		}
		StepBand = StepBand + 1;
		Finish = BottomBandReached && TopBandReached;
	}	
	return false;
}


/**
 **********************************************************************
 *************************************************************************
 *
 * Functions below are not needed
 * Functions are for statistics and benchmark tests
 *
 *************************************************************************
 *************************************************************************
 */

/**
 * @public stats_Band
 * Some informations on bands
 *
 * @param		
 * @return {array} Text result
 */
ClassSliceZone.prototype.stats_Band = function() {
	var resultText = [];
	var Total = 0;
	var str = "";
	this.statMyBands.Maxz = 0;
	this.statMyBands.MaxNumBand = 0;
	for (var my_band = 0; my_band < this.bands_.length; my_band++) {
		str = "";
		for (var i = 0; i < this.bands_[my_band].length; i++) {
			str = str + " " + this.bands_[my_band][i].properties.NAME
		}	
		console.log("Stat Band " + my_band + " count " , this.bands_[my_band].length, " regions: ", str);
		Total = Total + this.bands_[my_band].length;
		if (this.statMyBands.Maxz < this.bands_[my_band].length) {
			this.statMyBands.Maxz = this.bands_[my_band].length;
			this.statMyBands.MaxNumBand = my_band;
		}
	}
	resultText.push(["There are ", this.bands_.length, " bands"]);
	resultText.push(["There are ", Total, " regions"]);
	resultText.push(["There are ", Math.floor(Total / this.bands_.length), "average regions by band"]);
	resultText.push(["The Band ", this.statMyBands.MaxNumBand, " count the max region"]);
	resultText.push(["There are ", this.statMyBands.Maxz, "regions"]);
	
	return resultText;
}

/**
 * @public defaultTestingValues Init test 
 *
 * @param		
 * @return		
 */
ClassSliceZone.prototype.defaultTestingValues = function(mila, mala, miln, maln) {
	this.speedTest.NbTestSuc = 0;
	this.speedTest.NbTestFail = 0;

	this.speedTest._minLat = mila || this.MMCZ.minLat; // 33
	this.speedTest._maxLat = mala || this.MMCZ.maxLat; //41
	this.speedTest._minLng = miln || this.MMCZ.minLng; // -120
	this.speedTest._maxLng = maln || this.MMCZ.maxLng; // -78
	
	this.speedTest.stepLat = (this.speedTest._maxLat - this.speedTest._minLat) / 10;
	this.speedTest.stepLng = (this.speedTest._maxLng - this.speedTest._minLng) / 10;

}

/**
 * @public testingDiff
 * Testing if results differ between search
 *
 * @param		
 * @return {array} Text result
 */
ClassSliceZone.prototype.testingDiff = function() {
	var resultText = [];
	var NbTest = 0;	
	var found_gj, found_idx = false;
	var dept_gj, dept_idx = '';
	for (var lat = this.speedTest._minLat; lat < this.speedTest._maxLat; lat= lat + this.speedTest.stepLat) { 
		for (var lng = this.speedTest._minLng; lng < this.speedTest._maxLng; lng = lng + this.speedTest.stepLng) {
			NbTest++;

			found_gj = this.search_pointinside_gj({latitude: lat, longitude: lng});
			if (found_gj) {
				dept_gj = this.foundfeature.properties.NAME;
			}
				
			found_idx = this.search_pointinside({latitude: lat, longitude: lng});
			if (found_idx) {
				dept_idx = this.foundfeature.properties.NAME;
			}
			
			if (found_idx != found_gj) {				
				resultText.push(["Problem 1 diff search on ", lat, lng]);
			}
			if (found_idx || found_gj) {
				if (dept_idx != dept_gj) {					
					resultText.push(["Problem 2 diff search on ", lat, lng]);
				}
			}		
		}
	}	
	
	resultText.push(["Number of test ", NbTest, ""]);
	
	return resultText;
	
	// Step 0.1 : Problem diff search on lat 44.37370100000003 lng 4.986695000000002
	// Step 0.5 : Problem 2 diff search on lat 44.373701 lng 4.986695
}

/**
 * @public testingSpeedSeq
 * Testing speed sequential search
 *
 * @param		
 * @return		
 */
ClassSliceZone.prototype.testingSpeedSeq = function() {
	
	for (var lat = this.speedTest._minLat; lat < this.speedTest._maxLat; lat= lat + this.speedTest.stepLat) { 
		for (var lng = this.speedTest._minLng; lng < this.speedTest._maxLng; lng = lng + this.speedTest.stepLng) {
			if (this.search_pointinside_gj({latitude: lat, longitude: lng})) {
				this.speedTest.NbTestSuc++;
			}
			else {
				this.speedTest.NbTestFail++;
			}
		}
	}
}

/**
 * @public testingSpeedIdx
 * Testing speed band indexed search
 *
 * @param		
 * @return		
 */
ClassSliceZone.prototype.testingSpeedIdx = function() {

	for (var lat = this.speedTest._minLat; lat < this.speedTest._maxLat; lat= lat + this.speedTest.stepLat) {
		for (var lng = this.speedTest._minLng; lng < this.speedTest._maxLng; lng = lng + this.speedTest.stepLng) {
			if (this.search_pointinside({latitude: lat, longitude: lng})) {
				this.speedTest.NbTestSuc++;
			}
			else {
				this.speedTest.NbTestFail++;
			}			
		}
	}
}

/**
 *
 * -- Préparation --
 * 
 * 	- Calculer le centre de chaque zone : CenterZone = (CZLat, CZLong)
 * 	- Trouver le min longitude des centres : MinCZLong
 * 	- Trouver le min max latitude des centres, trouver la latitude central des min max (latitude mediane : MedCZLat).
 * 	- Pour chaque centre de zone  : calculer les distances entre le min des CZ et la longitude du centre
 * 		ceci sur la Latitude Mediane : DistCZ (MedCZLat, MinCZLong) et (MedCZLat, CZLong)
 * 		Soit LB une largeur de bande : 10 km
 * 		Numéro de bande / NumBande = Diviser entière de chaque DistCZ par PL.
 * 		Ranger chque zone dans un array BandeArray[NumBande]
 * 
 * -- Recherche --
 * 
 * 	- Le point de recherche SearchPoint = (SPLat, SPLong)
 * 	- Calcule de la distance DistSP = (MedCZLat, MinCZLong) et (MedCZLat, SPLong)
 * 	- Calcul de numéro de bande NumBandeSP = DistSP / LB
 * 	- Recherche si SP est dans une des  zones de BandeArray[NumBandeSP]
 * 	- Si pas trouver chercher dans NumBandeSP + 1 
 * 	- Si pas trouver chercher dans NumBandeSP - 1 
 * 	- Si pas trouver chercher dans NumBandeSP + 2 
 * 	- Si pas trouver chercher dans NumBandeSP - 2 
 * 	- etc ... recherche jusque derniere bande vers le bas et vers le haut
 * 
 * 
 * -- Format du geojson --
 * 
 * 	this.gj.features[i].properties.cz
 * 	this.gj.features[i].geometry.type === "Polygon"
 * 	this.gj.features[i].geometry.coordinates[0][0][j]
 * 	this.gj.features[i].geometry.type === "MultiPolygon"
 * 
 * 
 */
